package com.ubi.core.framework.request.model;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.validation.Errors;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;

import javax.servlet.ServletRequest;
import java.beans.PropertyEditorSupport;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class RequestModelMethodProcessor implements HandlerMethodArgumentResolver {

    private static final Map<Class<?>, Map<String, String>> TARGET_CLASS_PARAM_FIELD_MAPPING = new ConcurrentHashMap<Class<?>, Map<String, String>>();

    private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";

    @Autowired
    protected RequestMappingHandlerAdapter requestMappingHandlerAdapter;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(RequestModel.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        Object target = BeanUtils.instantiateClass(parameter.getParameterType());
        Map<String, String> paramFieldMapping = lookupParamFields(parameter.getParameterType());

        RequestModelDataBinder binder = new RequestModelDataBinder(target, parameter.getParameterName(),
            paramFieldMapping);
        requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(binder, webRequest);

        binder.registerCustomEditor(Date.class, new PropertyEditorSupport() {
            public void setAsText(String value) {
                try {
                    setValue(new SimpleDateFormat(DATE_FORMAT).parse(value));
                } catch (ParseException e) {
                    setValue(null);
                }
            }

            public String getAsText() {
                return new SimpleDateFormat(DATE_FORMAT).format((Date) getValue());
            }
        });

        binder.bind(webRequest.getNativeRequest(ServletRequest.class));

        validateIfApplicable(binder, parameter);
        if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
            throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
        }

        return binder.getTarget();
    }

    protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
        Annotation[] annotations = parameter.getParameterAnnotations();
        for (Annotation annotation : annotations) {
            if (annotation.annotationType().getSimpleName().startsWith("Valid")) {
                Object hints = AnnotationUtils.getValue(annotation);
                binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] { hints });
                break;
            }
        }
    }

    protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
        int i = parameter.getParameterIndex();
        Class<?>[] paramTypes = parameter.getMethod().getParameterTypes();
        boolean hasBindingResult = paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]);

        return !hasBindingResult;
    }

    Map<String, String> lookupParamFields(Class<?> targetClass) {
        if (TARGET_CLASS_PARAM_FIELD_MAPPING.containsKey(targetClass))
            return TARGET_CLASS_PARAM_FIELD_MAPPING.get(targetClass);

        List<Field> fields = lookupAllFields(targetClass);
        Map<String, String> paramFieldMap = new HashMap<>();
        for (Field field : fields) {
            Param annotation = field.getAnnotation(Param.class);
            if (annotation != null && !annotation.value().isEmpty()) {
                paramFieldMap.put(annotation.value(), field.getName());
            }
        }
        TARGET_CLASS_PARAM_FIELD_MAPPING.put(targetClass, paramFieldMap);
        return paramFieldMap;
    }

    List<Field> lookupAllFields(Class<?> targetClass) {
        Class<?> i = targetClass;
        List<Field> result = new ArrayList<>();
        while (i != null && i != Object.class) {
            for (Field field : i.getDeclaredFields()) {
                if (!field.isSynthetic()) {
                    result.add(field);
                }
            }
            i = i.getSuperclass();
        }
        return result;
    }
}
